package org.jpropelleralt.list.impl;

import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.change.ChangeDefault;
import org.jpropelleralt.collection.CollectionDeltaType;
import org.jpropelleralt.list.ListBox;
import org.jpropelleralt.list.ListChange;
import org.jpropelleralt.list.ListDelta;
import org.jpropelleralt.reaction.Reaction;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.ref.impl.RefDefault;

/**
 * Allows for creation of a {@link Ref} that
 * tracks a selection in a {@link ListBox}
 */
public class ListSelectionIndex {

	/**
	 * Create a {@link Ref} that will maintain a selection
	 * index in the {@link ListBox} value of a {@link Ref},
	 * where possible.
	 * Will select the next available element if selection is
	 * deleted, and will select first element by default when
	 * list is not empty.
	 * @param <T>		The type of element in the {@link ListBox} 
	 * @param listRef	The {@link Ref} to a {@link ListBox}
	 * 
	 * @return			A list selection index {@link Ref}
	 */
	public static <T> Ref<Integer> create(Ref<ListBox<T>> listRef) {
		return create(listRef, true, true, -1);
	}
	
	/**
	 * Create a {@link Ref} that will maintain a selection
	 * index in the {@link ListBox} value of a {@link Ref},
	 * where possible.
	 * Will select the next available element if selection is
	 * deleted, and will select first element by default when
	 * list is not empty.
	 * @param <T>		The type of element in the {@link ListBox} 
	 * @param listRef	The {@link Ref} to a {@link ListBox}
	 * @param initialSelection		The index to be selected initially (note this will be corrected
	 * 								to lie within the list if it does not, becoming -1 if list is empty)
	 * 
	 * @return			A list selection index {@link Ref}
	 */
	public static <T> Ref<Integer> create(Ref<ListBox<T>> listRef, int initialSelection) {
		return create(listRef, true, true, initialSelection);
	}
	
	/**
	 * Create a {@link Ref} that will maintain a selection
	 * index in the {@link ListBox} value of a {@link Ref},
	 * where possible.
	 * @param <T>		The type of element in the {@link ListBox} 
	 * @param listRef	The {@link Ref} to a {@link ListBox}
	 * @param postSelectOnDeletion	If true, the selection will be moved to 
	 * 								the next available item when the selected item
	 * 								is deleted. Otherwise, the selection will be
	 * 								reset when deleted.
	 * @param selectFirstByDefault	If true, the selection will be set to the first
	 * 								element when it is reset. Selection is reset when
	 * 								it is invalid, or the list is completely changed,
	 * 								or the selection is deleted and postSelectOnDeletion is false.
	 * @param initialSelection		The index to be selected initially (note this will be corrected
	 * 								to lie within the list if it does not, becoming -1 if list is empty)
	 * 
	 * @return			A list selection index {@link Ref}
	 */
	public static <T> Ref<Integer> create(Ref<ListBox<T>> listRef, boolean postSelectOnDeletion, boolean selectFirstByDefault, int initialSelection) {
		
		Ref<Integer> ref = RefDefault.create(initialSelection);
		ListSelectionReaction<T> reaction = new ListSelectionReaction<T>(listRef, ref, postSelectOnDeletion, selectFirstByDefault);

		//Keep reaction in scope while ref is in scope
		ref.features().registerReaction(reaction);
		
		listRef.features().addReaction(reaction);
		ref.features().addReaction(reaction);
		
		//Start out with reaction up to date and with cached listBox
		reaction.apply(true);
		
		return ref;
	}
	
	private static class ListSelectionReaction<T> implements Reaction {

		private final Ref<ListBox<T>> listRef;
		private ListBox<T> listBox = null;
		private final Ref<Integer> selectionRef;
		private final boolean postSelectOnDeletion;
		private final boolean selectFirstByDefault;
		
		Map<Box, Change> shallowChange = new IdentityHashMap<Box, Change>();
		
		private Action action = Action.NOTHING;
		private final List<ListChange> changesForAdjustment = new LinkedList<ListChange>();
		private boolean newUserSelection = false;

		/**
		 * Action to carry out when applied
		 */
		private enum Action {
			RESET,
			ADJUST,
			NOTHING
		}
		
		private ListSelectionReaction(Ref<ListBox<T>> listRef, Ref<Integer> selectionRef, boolean postSelectOnDeletion, boolean selectFirstByDefault) {
			this.listRef = listRef;
			this.selectionRef = selectionRef;
			this.postSelectOnDeletion = postSelectOnDeletion;
			this.selectFirstByDefault = selectFirstByDefault;
			shallowChange.put(selectionRef, ChangeDefault.shallowInstance());
		}
		
		@Override
		public void apply(boolean propagate) {
			
			//Find current index, and work out new index
			int index = selectionRef.get();
			int newIndex = index;

			//Reset
			if (action == Action.RESET) {
				newIndex = -1;
				
			//Adjust index
			} else if (action == Action.ADJUST) {
				//Check we still have correct cached listBox
				if (listBox != listRef.get()) {
					throw new RuntimeException("Invalid cached listBox in ListSelectionIndex");
				}
				newIndex = adjustIndex(newIndex);
			}

			//Clear action and changes for next time
			action = Action.NOTHING;
			changesForAdjustment.clear();

			//Make sure we have the current listBox stored for later
			listBox = listRef.get();
			
			//Check new index is valid 
			if (newIndex < -1) {
				newIndex = -1;
			} else if (newIndex >= listBox.size()) {
				newIndex = -1;
			}

			//Handle selecting index 0 by default, if required
			if (selectFirstByDefault && (newIndex == -1) && !listBox.isEmpty()) {
				newIndex = 0;
			}

			//If we have an actual change, make it
			if (index != newIndex) {
				selectionRef.reactionSet(newIndex, propagate);
			}
		}

		private int adjustIndex(int index) {
			
			//Apply effect of each delta
			for (ListChange change : changesForAdjustment) {
				for (ListDelta delta : change) {
					
					//Preserve selection even when altered
					if (delta.getType() == CollectionDeltaType.ALTERATION) {
						
					//Clear and complete changes just reset index, no need to
						//process further deltas
					} else if (delta.getType() == CollectionDeltaType.CLEAR) {
						return -1;
					} else if (delta.getType() == CollectionDeltaType.COMPLETE) {
						return -1;
						
					//Respond to deletion
					} else if (delta.getType() == CollectionDeltaType.DELETION) {
						
						//No change at all if we are before the first changed index
						if (index < delta.getFirstChangedIndex()) {
							//No alteration
							
						//Work out whether we are AFTER the removed range - e.g. if first removed is
						//2, and change size is -3, we removed 2,3,4 and so everything from (2-(-3) = 5) inclusive
						//is still present. We just need to shift selection back to allow for removed indices
						} else if (index >= delta.getFirstChangedIndex() - delta.getChangeSize()) {
							//Note "+", since the change size should be negative for a deletion, and we move index back
							index += delta.getChangeSize();
							
						//If we are in the removed range
						} else {
							
							//Select immediately after deletion if desired
							if (postSelectOnDeletion) {
								//If there is still an index after the deleted range, it will be
								//first changed index
								index = delta.getFirstChangedIndex();
								
								//If the deletion was up to the end of the list, index will be
								//outside list - in this case just use last index
								if (index >= delta.getNewSize()) {
									index = delta.getNewSize() - 1; 
								}
							//Otherwise just reset index
							} else {
								return -1;
							}
						}
					} else if (delta.getType() == CollectionDeltaType.INSERTION) {
						if (index < delta.getFirstChangedIndex()) {
							//No alteration
						} else {
							index += delta.getChangeSize();
						}				
					}
				}
			}
			
			return index;
		}

		@Override
		public Map<Box, Change> react(Map<Box, Change> changes) {
			//If we see a change to the selection but NOT to the
			//listRef, then we have a genuine new selection
			//index, therefore we should do nothing and apply immediately
			if (changes.containsKey(selectionRef) && !changes.containsKey(listRef)) {
				newUserSelection = true;
			}

			//We just make a shallow change - any change to anything we listen
			//to will probably make us adjust the selection
			return shallowChange;
		}

		@Override
		public boolean completeChanges(Map<Box, Change> changes) {
			
			//If we know what the listbox is, and we are not planning to
			//just reset, then adjust for any list changes
			if (listBox!=null && action != Action.RESET) {
				Change change = changes.get(listBox);
				if (change instanceof ListChange) {
					ListChange listChange = (ListChange) change;
					changesForAdjustment.add(listChange);
					action = Action.ADJUST;
				}
			}
			
			//But more importantly...
			//If listRef now contains a different ListBox, we
			//should reset the selection, and we don't know the listBox any more
			Change changeToListRef = changes.get(listRef);
			if (changeToListRef != null && changeToListRef.shallow()) {
				listBox = null;
				action = Action.RESET;
				changesForAdjustment.clear();
			}

			//But more importantly...
			//If we have a new user selection, then we should just keep
			//it, by doing nothing, and requesting immediate application
			if (newUserSelection) {
				action = Action.NOTHING;
				changesForAdjustment.clear();
				newUserSelection = false;
				return true;
			}

			//Otherwise, we need to apply immediately if cached listBox is now invalid
			return (listBox == null);
		}
	}
}
